<?php

namespace slinks\core\dependencyInjectionExt;

use slinks\core\DependencyInjection\ContainerBuilder;
use slinks\common\Finder\Finder;
use slinks\common\io\ClassReader;
use slinks\common\io\InterfaceCollectingClassVisitor;
use slinks\core\dependencyInjectionExt\ApplicationFileVisitor;
use slinks\common\io\FileSystemReader;

/**
 * Various container utility functions
 *
 * @author navid
 */
class ContainerUtil {
    
    /**
     * Sets the given value for the given array parameter and key. 
     * @param ContainerBuilder $container
     * @param type $parameterName the name of the parameter to set the key and value for.
     * @param type $key the key to use for the new array parameter
     * @param type $value the value to use for the new array parameter
     * @throws \InvalidArgumentException if the Parameter referenced by $parameterName is not an array.
     */
    public static function setValueForArrayParameter(ContainerBuilder $container, $parameterName, $key, $value) {

        $arrayParameter = array();

        if ($container->hasParameter($parameterName)) {
            $arrayParameter = $container->getParameter($parameterName);
        }

        if (!is_array($arrayParameter)) {
            throw new \InvalidArgumentException('Parameter ' . $parameterName . ' is not and Array');
        }

        $arrayParameter[$key] = $value;

        $container->setParameter($parameterName, $arrayParameter);
    }

    /**
     * Adds the given value for the given array parameter
     * @param ContainerBuilder $container
     * @param type $parameterName the name of the parameter to set the key and value for.
     * @param type $value the value to use for the new array parameter
     * NOTE: if the current value for the parameter is not an array it will first be converted to an array before adding the new parameter.
     */
    public static function addValueToArrayParameter(ContainerBuilder $container, $parameterName, $value) {

        $arrayParameter = array();

        if ($container->hasParameter($parameterName)) {
            $param = $container->getParameter($parameterName);

            if (is_array($param)) {
                $arrayParameter = $param;
            } else {
                $arrayParameter[] = $param;
            }
        }

        $arrayParameter[] = $value;

        $container->setParameter($parameterName, $arrayParameter);
    }

    /**
     * The application is a hybrid container extension point. It allows you to configure services, extensions and compilers in one shot.
     * 
     * The typical directory structure for a application is as follows. It is good practice to name the application directory and the application service file the same.
     * 
     * {application}
     * --extensions
     * ----myextension
     * ------compiler
     * --------CompilerImplementation.php
     * ----ExtensionImplementation.php
     * --{application}.config.xml
     * 
     * The extensions directory contains any container extensions that need to be added. 
     * An extension can define additional compilers as well the compilers must be contained within the extensions/compiler directory.
     * The extensions and compiler directories and implementations can be ommited. 
     *
     * The only requirement is that there is a {application}.services.xml file within each directory that has a application. 
     * {application} automatically becomes the name of the application when needed for any of the other utility methods.
     * 
     * When defining a application certain parameters are automatically registered which are then available to the user.
     * 
     * Parameters Available:
     * 
     * {application}.root.dir = points to the directory where the {application}.services.xml is found.
     * 
     * 
     * @param ContainerBuilder $container to add the application to.
     * @param mixed $applicationDirs the path to search for {application}.services.xml that define the applications. Or an array of paths if desired. 
     * @param bool  $autoRegisterNamespace optional parameter if true will automatically register a namespace with the root directory as the namespace prefix. Default is true.
     *                              Basically this removes the need to do this for every application. 
     *                              However this also imposes that all application code's root namespace must be the same as the application directory name. 
     *                              
     * <code>
     *                              $loader = new \slinks\common\UniversalClassLoader();
     *
     *                              $internalNamespaces = array(
     *                                      'applicationDirectoryName'         =>  'applicationDirectoryPath' // Note this info would be replaced with the application info.
     *                                      );
     *                              $loader->registerNamespaces(array_merge($internalNamespaces,$this->classNamespaces));
     *
     *                              $loader->register();
     * </code>
     *                              @see slinks\common\UniversalClassLoader for more information. 
     * @param string $fileSuffix optional parameter that defines the file suffix that will be used when looking for application definitions.
     *                   defaults to services.xml. This means all application files must be named {application}.config.xml. 
     *                   This can be changed by changing the value of this parameter.
     * @param int $depth optional parameter that defines how deep the folder structure will be searched.
     */
    public static function addApplications(ContainerBuilder $container, $applicationDirs, $autoRegisterNamespace=true, $fileSuffix='.config.xml', $depth=null) {

        $filename = '/.*' . $fileSuffix . '/';

        $reader = new FileSystemReader(new ApplicationFileVisitor($container, $fileSuffix, $autoRegisterNamespace));

        $finder = new Finder();
        if ($depth != null) {
            $reader->read($finder->files()->depth('< ' . $depth)->name($filename)->in($applicationDirs));
        } else {
            $reader->read($finder->files()->name($filename)->in($applicationDirs));
        }
    }

    /**
     * Adds any container extensions that can be found at the path.
     * Will additionally add any compilers found within the extensionsDir/compiler paths.
     * 
     * @param ContainerBuilder $container the container to add the extensions to.
     * @param string|array $extensionDirs the directory or array of directories to search for extensions. 
     */
    public static function addExtensions(ContainerBuilder $container, $extensionDirs) {

        // find all extensions in the given directories
        $visitor = new InterfaceCollectingClassVisitor(array('slinks\\core\\DependencyInjection\\Extension\\ExtensionInterface'));
        $reader = new ClassReader($visitor);
        $finder = new Finder();
        $reader->read($finder->files()->name('*.php')->depth('< 2')->in($extensionDirs));

        // get all extensions
        if ($visitor->getClasses() != null) {
            foreach ($visitor->getClasses() as $class) {
                if ($class->IsInstantiable()) {
                    $extension = $class->newInstance();
                    $container->setParameter('slinks.extension.'.$extension->getAlias(), dirname($class->getFileName()));
                    $container->registerExtension($extension);
                }
            }
        }

        // extension compilers
        $finder = new Finder();
        self::addCompilers($container, $finder->files()->pathName('/.*compiler.*\.php/')->depth('< 3')->in($extensionDirs));
    }

    /**
     * Adds any compilers found for the given pre configured finder. 
     * All compilers added must implement slinks\\core\\dependencyInjectionExt\\compiler\\ICompilerPass to be considered valid
     * 
     * NOTE: all classes scanned must be configured with an autoloader or else they will not be configured.
     * 
     * @param ContainerBuilder $container the container to add the compilers to.
     * @param Finder $finder the preconfigured finder to resolve the slinks\\core\\dependencyInjectionExt\\compiler\\ICompilerPass obejcts.
     */
    public static function addCompilers(ContainerBuilder $container, Finder $finder) {

        $visitor = new InterfaceCollectingClassVisitor(array('slinks\\core\\dependencyInjectionExt\\compiler\\ICompilerPass'));
        $reader = new ClassReader($visitor);

        $reader->read($finder);

        if ($visitor->getClasses() != null) {
            foreach ($visitor->getClasses() as $class) {
                if ($class->IsInstantiable()) {
                    $pass = $class->newInstance();
                    $container->addCompilerPass($pass, $pass->getType());
                }
            }
        }
    }

}

